/*
 * Copyright 2007. Mount Sinai Hospital, Toronto, Canada.
 * 
 * Licensed under the GNU Lesser General Public License, Version 2. You
 * can find a copy of the license at:
 * 
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
 * 
 * IN NO EVENT SHALL MOUNT SINAI HOSPITAL BE LIABLE TO ANY PARTY FOR DIRECT, 
 * INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST 
 * PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, 
 * EVEN IF MOUNT SINAI HOSPITAL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * 
 * MOUNT SINAI HOSPITAL SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE AND 
 * ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS IS". 
 * MOUNT SINAI HOSPITAL HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, 
 * UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 
 */
package com.sinai.mshab.server.sql;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import com.sinai.mshab.client.MSHABConfig;
import com.sinai.mshab.client.ui.Key;

/**
 * The abstract class that offers services to SQL objects and offers a layer of
 * abstraction.
 * 
 * @author Arthur Kalmenson
 */
public abstract class SQLObject {

	/**
	 * An Set of {@link DbField}s.
	 */
	protected Set dbFields = new HashSet();

	/**
	 * The group of filters for this SQLObject. We assume that the operator
	 * between them is AND.
	 */
	protected DbFilterGroup dbFilters = new DbFilterGroup("AND");

	/**
	 * The {@link Connection} to the SQL database.
	 */
	protected Connection connection;

	/**
	 * Returns a {@link PreparedStatement} that is constructed by parsing and
	 * combining dbFields and dbFilters.
	 * 
	 * @return a {@link PreparedStatement} that is constructed by parsing and
	 *         combining dbFields and dbFilters.
	 * @throws SQLException
	 *             throws a SQL exception if any SQL goes wrong.
	 */
	public abstract PreparedStatement getPreparedStatement()
			throws SQLException;

	/**
	 * Gets the {@link PreparedStatement} of this SQLObject and runs
	 * executeUpdate on it. Returns the numbers of affected rows. If a
	 * SQLException occurs we throw it.
	 * 
	 * @return the numbers of affected rows after executing an update.
	 * @throws SQLException
	 *             if an exception occurs, throw it.
	 */
	protected int executeUpdate() throws SQLException {
		PreparedStatement statement = getPreparedStatement();
		if (statement == null) {
			throw new IllegalStateException(
					MSHABConfig.MSHAB_ERROR_EXECUTE_NULL);
		}

		// get the number of rows effected.
		int rowsEffected = statement.executeUpdate();

		// close the statement since we've already executed it.
		statement.close();

		return rowsEffected;
	}

	/**
	 * Returns all the tables that are referred to in dbFields and dbFilters.
	 * 
	 * @return a {@link Set} with the table names.
	 */
	public Set getTableNames() {

		Set tables = new HashSet();

		// go through all the DbFields and grab their associated table.
		Iterator fieldIter = dbFields.iterator();
		while (fieldIter.hasNext()) {
			DbField field = (DbField) fieldIter.next();
			tables.add(field.getTable());
		}

		// go through all the WhereClauses and grab their associated table.
		Iterator whereIter = dbFilters.getFlatSet().iterator();
		while (whereIter.hasNext()) {
			DbFilter where = (DbFilter) whereIter.next();
			tables.add(where.getTable());
		}

		return tables;
	}

	/**
	 * Adds a {@link Key} to this SQLObject. This is similar to the
	 * {@link #add(String, String, String)} method in the sense that it will be
	 * used in INSERT.
	 * 
	 * @param dbKey
	 *            the Key which we want to store.
	 */
	protected void add(Key dbKeys) {

		// iterator through all the keys and use our existing add method.
		Iterator keys = dbKeys.getKeys().keySet().iterator();
		while (keys.hasNext()) {
			String field = (String) keys.next();
			String value = (String) dbKeys.getKeys().get(field);
			add(dbKeys.getTableName(), field, value);
		}
	}

	/**
	 * Adds fieldName and value to this SQLObject. Note that if the fieldName is
	 * null or empty, an {@link IllegalArgumentException} will be thrown.
	 * 
	 * @param tableName
	 *            the name of the table where the field is stored.
	 * @param fieldName
	 *            the name of a field in the database.
	 * @param value
	 *            the value the field should take.
	 */
	protected void add(String tableName, String fieldName, String value) {
		if (invalidArgument(fieldName)) {
			throw new IllegalArgumentException(
					MSHABConfig.MSHAB_ERROR_INVALID_FIELD);
		}

		// create the DbField and store it in the Set of DbFields.
		DbField field = new DbField(tableName, fieldName, value);
		dbFields.add(field);
	}

	/**
	 * Adds <code>group</code> to this SQLObject's group of DbFilters.
	 * 
	 * @param group
	 *            another DbFilterGroup to add to our current group.
	 */
	protected void addWhere(DbFilterGroup group) {
		dbFilters.add(group);
	}

	/**
	 * Adds a {@link Key} to this SQLObject's WHERE clause. Note that we
	 * <i>assume</i> the operator between the field and the value in the Key is
	 * "equal".
	 * 
	 * @param dbKeys
	 *            the Key that we wish to add to the WHERE clause.
	 */
	protected void addWhere(Key dbKeys) {

		// iterator through all the keys and use our existing addWhere method.
		Iterator keys = dbKeys.getKeys().keySet().iterator();
		while (keys.hasNext()) {
			String field = (String) keys.next();
			String value = (String) dbKeys.getKeys().get(field);
			addWhere(dbKeys.getTableName(), field, value, "=");
		}
	}

	/**
	 * Adds a new filter to the SQL statement. The filter (the WHERE clause)
	 * will be appended to any existing conditions. Therefore, if the addWhere
	 * recieves "tableA", "fieldA", "=" and "val", then the SQL query that it
	 * will yield will contain "... WHERE ... tableA.fieldA = val ...". Note
	 * that the operator must be a regular SQL operator otherwise a
	 * {@link RuntimeException} will be thrown.
	 * 
	 * @param tableName
	 *            the table name associated with the field.
	 * @param fieldName
	 *            the field which we will use as a filter.
	 * @param value
	 *            the value which we want to field to have.
	 * @param operator
	 *            the operator between the field name and the value.
	 */
	protected void addWhere(String tableName, String fieldName, String value,
			String operator) {
		DbFilter where = new DbFilter(tableName, fieldName, value, operator);
		dbFilters.add(where);
	}

	/**
	 * Returns a where clause that appends all the key, expected value pairs
	 * together.
	 * 
	 * @param useTableNames
	 *            flag to indicate whether to precede field names with table
	 *            names.
	 * @return a where clause that appends all the key, expected value pairs
	 *         together.
	 */
	protected String createWhereUnknowns(boolean useTableNames) {

		String sql = "";

		if (dbFilters.isEmpty()) {
			return sql;
		}

		sql += " WHERE ";

		// add the SQL from our DbFilterGroup.
		sql += dbFilters.getSQL();

		return sql;
	}

	/**
	 * Using a {@link Map} containing {@link DbField}s, we use
	 * {@link PreparedStatement}'s statement method to set the appropriate
	 * value for each field name. The setString(int, String) method is used
	 * corresponding to on of the fundamental assumptions of mshab, that all
	 * fields are Strings (except for blobs).
	 * 
	 * The filling in begins at startIndex and a new index equal to startIndex
	 * plus the number of fields filled in is returned.
	 * 
	 * @param statement
	 *            the {@link PreparedStatement} that will assign values to field
	 *            names.
	 * @param fieldValue
	 *            a {@link Map} of {@link DbField}s to be used to fill.
	 * @param startIndex
	 *            the index where to start filling in values.
	 * @return a new index which is equal to startIndex plus the number of
	 *         values we filled.
	 * @throws SQLException
	 *             if something goes wrong with statement, a SQL exception will
	 *             be thrown.
	 */
	protected int fillUnknowns(PreparedStatement statement, Set fieldValue,
			int startIndex) throws SQLException {

		int index = startIndex;

		// iterate through all the fields and use the PreparedStatement to fill
		// the fields with their respective information. The values will be
		// filled in the correct order as this is the order we used to construct
		// the SQL with question marks in it.
		Iterator whereIter = fieldValue.iterator();
		while (whereIter.hasNext()) {
			DbField field = (DbField) whereIter.next();
			statement.setString(index, field.getValue());
			index++;
		}

		return index;
	}

	/**
	 * Returns true if arg is null or empty, and false otherwise.
	 * 
	 * @param arg
	 *            the String argument to check.
	 * @return true if arg is null or empty, and false otherwise.
	 */
	protected boolean invalidArgument(String arg) {
		return (arg == null || arg.trim().isEmpty());
	}
}
